/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.codegen.core;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import com.querydsl.codegen.Property;
import com.querydsl.codegen.SerializerConfig;
import com.querydsl.codegen.utils.CodeWriter;
import com.querydsl.codegen.utils.model.ClassType;
import com.querydsl.codegen.utils.model.Parameter;
import com.querydsl.codegen.utils.model.SimpleType;
import com.querydsl.codegen.utils.model.Type;
import com.querydsl.codegen.utils.model.TypeCategory;
import com.querydsl.codegen.utils.model.Types;
import com.querydsl.core.util.BeanUtils;
import com.querydsl.sql.ColumnMetadata;

import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;

/**
 * @author <a href=mailto:hedyn@foxmail.com>HeDYn</a>
 * @see com.querydsl.codegen.BeanSerializer
 */
public class OrmliteEntityBeanSerializer {

    private static final Function<Property, Parameter> PROPERTY_TO_PARAMETER
            = input -> new Parameter(input.getName(), input.getType());

    private final boolean propertyAnnotations;
    private final List<Type> interfaces = new ArrayList<>();
    private final String javadocSuffix;

    private boolean addToString, addFullConstructor;
    private Class<?> daoClass = null;
    private Comparator<Property> columnComparator;

    public OrmliteEntityBeanSerializer() {
        this(true, " is a ORMLite bean type");
    }

    public OrmliteEntityBeanSerializer(String javadocSuffix) {
        this(true, javadocSuffix);
    }

    public OrmliteEntityBeanSerializer(boolean propertyAnnotations) {
        this(propertyAnnotations, " is a ORMLite bean type");
    }

    public OrmliteEntityBeanSerializer(boolean propertyAnnotations, String javadocSuffix) {
        this.propertyAnnotations = propertyAnnotations;
        this.javadocSuffix = javadocSuffix;
    }

    public void serialize(EntityModel model, SerializerConfig serializerConfig, CodeWriter writer)
            throws IOException {
        String simpleName = model.getSimpleName();

        // package
        if (!model.getPackageName().isEmpty()) {
            writer.packageDecl(model.getPackageName());
        }

        List<Type> interfaceList = new ArrayList<Type>();
        interfaceList.addAll(interfaces);
        interfaceList.addAll(model.getInterfaces());

        // imports
        Set<String> importedClasses = getAnnotationTypes(model);
        for (Type iface : interfaceList) {
            importedClasses.add(iface.getFullName());
        }
        if (model.hasLists()) {
            importedClasses.add(List.class.getName());
        }
        if (model.hasCollections()) {
            importedClasses.add(Collection.class.getName());
        }
        if (model.hasSets()) {
            importedClasses.add(Set.class.getName());
        }
        if (model.hasMaps()) {
            importedClasses.add(Map.class.getName());
        }
        if (addToString && model.hasArrays()) {
            importedClasses.add(Arrays.class.getName());
        }
        if (daoClass != null) {
            importedClasses.add(daoClass.getName());
        }
        if (!importedClasses.isEmpty()) {
            writer.importClasses(importedClasses.toArray(new String[0]));
        }
        writer.importPackages(DatabaseField.class.getPackage().getName(), DatabaseTable.class.getPackage().getName());

        // javadoc
        writer.javadoc(simpleName + javadocSuffix
                + ". Corresponds to the database table \""
                + model.getData().get("table") + "\"");

        // header
        for (Annotation annotation : model.getAnnotations()) {
            writer.annotation(annotation);
        }

        StringBuilder sb = new StringBuilder("@DatabaseTable(tableName = \"");
        sb.append((String) model.getData().get("table"));
        sb.append("\"");
        if (daoClass != null) {
            sb.append(", daoClass = ");
            sb.append(daoClass.getSimpleName());
            sb.append(".class");
        }
        sb.append(")");
        writer.line(sb.toString());

        Type superType = null;
        if (model.getSuperType() != null) {
            superType = model.getSuperType().getType();
        }
        if (!interfaceList.isEmpty()) {
            Type[] ifaces = interfaceList.toArray(new Type[0]);
            writer.beginClass(model, superType, ifaces);
        } else {
            writer.beginClass(model, superType);
        }

        bodyStart(model, writer);

        if (addFullConstructor) {
            addFullConstructor(model, writer);
        }

        List<Property> properties = CodegenUtils.getProperties(model, columnComparator);
        serializeProperties(model, properties, writer);
        if (addToString) {
            addToString(properties, writer);
        }
        addHashCodeAndEquals(writer);

        bodyEnd(model, writer);

        writer.end();
    }

    protected void serializeProperties(EntityModel model, List<Property> properties, CodeWriter writer)
            throws IOException {
        writer.javadoc("The columns of table \"" + model.getData().get("table") + "\"");
        SimpleType type = new SimpleType("$");
        SuperType supertype = model.getSuperType();
        if (supertype == null) {
            writer.beginInterface(type);
        } else {
            SimpleType interfaceType = new SimpleType(TypeCategory.ENTITY,
                    supertype.getType().getFullName() + ".$",
                    supertype.getType().getPackageName(),
                    "$", false, false);
            writer.beginInterface(type, interfaceType);
        }
        for (Property property : properties) {
            ColumnMetadata metadata = (ColumnMetadata) property.getData().get("COLUMN");
            String columnName = metadata.getName();
            writer.javadoc(columnName);
            writer.publicStaticFinal(Types.STRING, property.getEscapedName(), "\"" + columnName + "\"");
        }
        writer.end();

        // fields
        StringBuilder sb = new StringBuilder();
        for (Property property : properties) {
            if (propertyAnnotations) {
                for (Annotation annotation : property.getAnnotations()) {
                    writer.annotation(annotation);
                }
            }
            sb.setLength(0);
            sb.append(property.getEscapedName());
            switch (getJdbcType(property)) {
                case java.sql.Types.DATE:
                    sb.append(", dataType = DataType.DATE, format=\"yyyy-MM-dd\"");
                    break;
                case java.sql.Types.TIMESTAMP:
                    sb.append(", dataType = DataType.DATE, format=\"yyyy-MM-dd HH:mm:ss\"");
                    break;
                case java.sql.Types.TIME:
                case java.sql.Types.TIME_WITH_TIMEZONE:
                case java.sql.Types.TIMESTAMP_WITH_TIMEZONE:
                    sb.append(", dataType = DataType.DATE, format=\"HH:mm:ss\"");
                    break;
                default:
                    break;
            }
            ColumnMetadata metadata = (ColumnMetadata) property.getData().get("COLUMN");
            if (!metadata.isNullable()) {
                sb.append(", canBeNull = false");
            }
            writer.line("@DatabaseField(columnName = $.", sb.toString(), ")");
            writer.privateField(property.getType(), property.getEscapedName());
        }

        // accessors
        for (Property property : properties) {
            String propertyName = property.getEscapedName();
            // getter
            writer.beginPublicMethod(property.getType(), "get" + BeanUtils.capitalize(propertyName));
            writer.line("return ", propertyName, ";");
            writer.end();
            // setter
            Parameter parameter = new Parameter(propertyName, property.getType());
            writer.beginPublicMethod(Types.VOID, "set" + BeanUtils.capitalize(propertyName), parameter);
            writer.line("this.", propertyName, " = ", propertyName, ";");
            writer.end();
        }
    }

    private ColumnMetadata getColumnMetadata(Property property) {
        return (ColumnMetadata) property.getData().get("COLUMN");
    }

    private int getJdbcType(Property property) {
        return getColumnMetadata(property).getJdbcType();
    }

    protected void addFullConstructor(EntityModel model, CodeWriter writer) throws IOException {
        // public empty constructor
        writer.beginConstructor();
        writer.end();

        // full constructor
        writer.beginConstructor(model.getProperties(), PROPERTY_TO_PARAMETER);
        for (Property property : model.getProperties()) {
            writer.line("this.", property.getEscapedName(), " = ", property.getEscapedName(), ";");
        }
        writer.end();
    }

    protected void addHashCodeAndEquals(CodeWriter writer) throws IOException {
        // hashCode
        writer.line("@Override");
        writer.beginPublicMethod(Types.INT, "hashCode");
        writer.line("return super.hashCode();");
        writer.end();
        // equals
        Parameter parameter = new Parameter("obj", new ClassType(Object.class));
        writer.line("@Override");
        writer.beginPublicMethod(Types.BOOLEAN_P, "equals", parameter);
        writer.line("return super.equals(obj);");
        writer.end();
    }

    protected void addToString(List<Property> properties, CodeWriter writer) throws IOException {
        if (!properties.isEmpty()) {
            writer.line("@Override");
            writer.beginPublicMethod(Types.STRING, "toString");
            writer.line("StringBuilder sb = new StringBuilder();");
            writer.line("sb.append(super.toString());");
            for (Property property : properties) {
                String propertyName = property.getEscapedName();
                writer.line("sb.append(\",", propertyName, "=\").append(", propertyName, ");");
            }
            writer.line("return sb.toString();");
            writer.end();
        }
    }

    protected void bodyStart(EntityModel model, CodeWriter writer) throws IOException {
        SuperType supertype = model.getSuperType();
        if (supertype != null) {
            Class<?> superClass = supertype.getType().getJavaClass();
            if (Serializable.class.isAssignableFrom(superClass)) {
                writer.privateStaticFinal(Types.LONG_P, "serialVersionUID", "1L");
            }
        }
    }

    protected void bodyEnd(EntityModel model, CodeWriter writer) throws IOException {
    }

    private Set<String> getAnnotationTypes(EntityModel model) {
        Set<String> imports = new TreeSet<>();
        for (Annotation annotation : model.getAnnotations()) {
            imports.add(annotation.annotationType().getName());
        }
        if (propertyAnnotations) {
            for (Property property : model.getProperties()) {
                for (Annotation annotation : property.getAnnotations()) {
                    imports.add(annotation.annotationType().getName());
                }
            }
        }
        return imports;
    }

    public void addInterface(Class<?> iface) {
        interfaces.add(new ClassType(iface));
    }

    public void addInterface(Type type) {
        interfaces.add(type);
    }

    public void setAddToString(boolean addToString) {
        this.addToString = addToString;
    }

    public void setAddFullConstructor(boolean addFullConstructor) {
        this.addFullConstructor = addFullConstructor;
    }

    public void setDaoClass(Class<?> daoClass) {
        this.daoClass = daoClass;
    }

    public void setColumnComparator(Comparator<Property> columnComparator) {
        this.columnComparator = columnComparator;
    }
}
